package jmine.tec.scheduling.quartz;

import static jmine.tec.scheduling.SchedulingMessages.ERROR_CREATING_TRIGGER_INVALID_CRON_EXPRESSION;
import static jmine.tec.scheduling.SchedulingMessages.ERROR_DELETING_JOB;
import static jmine.tec.scheduling.SchedulingMessages.ERROR_SCHEDULING_JOB;

import java.text.ParseException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import jmine.tec.scheduling.ScheduledTaskManager;
import jmine.tec.scheduling.SchedulingException;
import jmine.tec.scheduling.dao.ScheduledTaskExecutionDAO;
import jmine.tec.scheduling.domain.ScheduledTask;

import org.quartz.CronTrigger;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;

import bancosys.tec.exception.informer.UserFriendlyExceptionInformer;
import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.rtm.impl.RtmController;
import bancosys.tec.utils.date.Clock;

/**
 * Manages scheduled tasks, keeping all timers matching the task's persistent state.
 * 
 * @author ricardo.abe
 */
public class QuartzScheduledTaskManager implements BeanFactoryAware, ScheduledTaskManager {

    private static final String JOB_GROUP = "jmine-tec-scheduling";

    private BeanFactory beanFactory;

    private Scheduler scheduler;

    private BaseDAO<ScheduledTask> scheduledTaskDAO;

    private Map<String, Long> jobs = new HashMap<String, Long>();

    /**
     * {@inheritDoc}
     */
    public synchronized void synchronizeTimers() {
        Map<String, ScheduledTask> currentJobs = new HashMap<String, ScheduledTask>();

        for (ScheduledTask task : this.scheduledTaskDAO.findAll()) {
            currentJobs.put(task.getMnemonic(), task);
        }

        Set<String> allJobMenmonics = new HashSet<String>();
        allJobMenmonics.addAll(this.jobs.keySet());
        allJobMenmonics.addAll(currentJobs.keySet());

        for (String mnemonic : allJobMenmonics) {
            ScheduledTask currentTask = currentJobs.get(mnemonic);
            if (!this.jobs.containsKey(mnemonic)) {
                if (currentTask.getActive()) {
                    this.scheduleJob(currentTask);
                }
            } else if (currentTask == null) {
                this.removeJob(mnemonic);
            } else if (!this.jobs.get(mnemonic).equals(currentTask.getVersion())) {
                this.updateJob(currentTask);
            }
        }
    }

    /**
     * Agenda a execução da tarefa via Quartz.
     * 
     * @param task task
     */
    private void scheduleJob(ScheduledTask task) {
        JobDetail job = this.createJob(task);
        CronTrigger trigger = this.createTrigger(task);
        try {
            this.scheduler.scheduleJob(job, trigger);
        } catch (SchedulerException e) {
            throw new SchedulingException(ERROR_SCHEDULING_JOB.create(task.getMnemonic()), e);
        }
        this.jobs.put(task.getMnemonic(), task.getVersion());
    }

    /**
     * Cria trigger do quartz.
     * 
     * @param task task
     * @return trigger
     */
    private CronTrigger createTrigger(ScheduledTask task) {
        CronTrigger trigger = new CronTrigger(task.getMnemonic(), JOB_GROUP);
        try {
            trigger.setCronExpression(task.getCron());
        } catch (ParseException e) {
            throw new SchedulingException(ERROR_CREATING_TRIGGER_INVALID_CRON_EXPRESSION.create(task.getMnemonic(), task.getCron()), e);
        }
        return trigger;
    }

    /**
     * Cria jobDetail do quartz.
     * 
     * @param task task
     * @return JobDetail
     */
    private JobDetail createJob(ScheduledTask task) {
        JobDataMap map = new JobDataMap();

        map.put("task", task);
        map.put("clock", this.getBean("wallClock", Clock.class));
        map.put("rtmController", this.getBean("rtmController", RtmController.class));
        map.put("dao", this.getBean("scheduledTaskExecutionDAO", ScheduledTaskExecutionDAO.class));
        map.put("informer", this.getBean("userFriendlyExceptionInformer", UserFriendlyExceptionInformer.class));

        JobDetail job = new JobDetail(task.getMnemonic(), JOB_GROUP, ScheduledJob.class);
        job.setJobDataMap(map);
        return job;
    }

    /**
     * Faz o reagendamento da tarefa.
     * 
     * @param task task
     */
    private void rescheduleJob(ScheduledTask task) {
        this.removeJob(task.getMnemonic());
        this.scheduleJob(task);
    }

    /**
     * Elimina a tarefa da lista de execução do Quartz.
     * 
     * @param mnemonic mnemonic
     */
    private void removeJob(String mnemonic) {
        try {
            this.scheduler.deleteJob(mnemonic, JOB_GROUP);
        } catch (SchedulerException e) {
            throw new SchedulingException(ERROR_DELETING_JOB.create(mnemonic), e);
        }
        this.jobs.remove(mnemonic);
    }

    /**
     * Searches for a bean
     * 
     * @param <B> bean type
     * @param name mnemonic
     * @param requiredType requiredType
     * @return bean
     */
    private <B> B getBean(String name, Class<B> requiredType) {
        return requiredType.cast(this.beanFactory.getBean(name, requiredType));
    }

    /**
     * Atualiza um job, se ele encontra-se ativo. Caso contrário, remove job do agendador.
     * 
     * @param task task
     * @throws SchedulerException
     * @throws ParseException
     */
    private void updateJob(ScheduledTask task) {
        if (!task.getActive()) {
            this.removeJob(task.getMnemonic());
        } else {
            this.rescheduleJob(task);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void setBeanFactory(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    /**
     * @param scheduler the scheduler to set
     */
    public void setScheduler(Scheduler scheduler) {
        this.scheduler = scheduler;
    }

    /**
     * @param scheduledTaskDAO the scheduledTaskDAO to set
     */
    public void setScheduledTaskDAO(BaseDAO<ScheduledTask> scheduledTaskDAO) {
        this.scheduledTaskDAO = scheduledTaskDAO;
    }
}
